package jp.co.nes.awssupport.report;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.spec.InvalidKeySpecException;
import java.time.YearMonth;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jp.co.nes.awssupport.access.dao.SforceManageDao;
import jp.co.nes.awssupport.common.base.AbstractClient;
import jp.co.nes.awssupport.common.base.CommonException;
import jp.co.nes.awssupport.common.bean.ContractInformationBean;
import jp.co.nes.awssupport.common.bean.UsageHistoryBean;
import jp.co.nes.awssupport.common.utility.AppInfoUtil;
import jp.co.nes.awssupport.common.utility.DateUtil;
import jp.co.nes.awssupport.common.utility.DbConnUtil;
import jp.co.nes.awssupport.report.constant.ReportType;
import jp.co.nes.awssupport.report.utility.ReportHelper;

/**
 * レポートを出力するクラスで使用する抽象クラスです。
 */
public abstract class AbstractReportClient extends AbstractClient {

    /** ロガー */
    private static Logger logger = LoggerFactory.getLogger(AbstractReportClient.class);

    /** 処理対象年月 */
    private YearMonth targetYearMonth;

    /** レポートヘルパー */
    private ReportHelper reportHelper;

    /**
     * デフォルトコンストラクタです。
     */
    protected AbstractReportClient() {
    }

    /**
     * レポートヘルパーを引数に持つコンストラクタです。
     *
     * @param reportHelper レポートヘルパー
     */
    protected AbstractReportClient(ReportHelper reportHelper) {
        this.reportHelper = reportHelper;
    }

    /**
     * 処理名を返却します。<br>
     * 返却した処理名はログ出力時、例外のメッセージの設定で使用されます。
     *
     * @return 処理名
     */
    protected abstract String getProcessName();

    /**
     * レポートタイプを返却します。
     *
     * @return レポートタイプ
     */
    protected abstract ReportType getReportType();

    /**
     * コマンドライン引数で設定された処理対象年月差月数を返却します。
     *
     * @return コマンドライン引数で設定された処理対象年月差月数
     */
    protected abstract long getDiffMonth();

    /**
     * 処理対象年月を返却します。
     *
     * @return 処理対象年月
     */
    protected YearMonth getTargetYearMonth() {
        return targetYearMonth;
    }

    /**
     * 処理対象年月を設定します。
     *
     * @param targetYearMonth 処理対象年月
     */
    protected void setTargetYearMonth(YearMonth targetYearMonth) {
        this.targetYearMonth = targetYearMonth;
    }

    /**
     * s3にアップロードする際に使用するバケット名を返却します。
     *
     * @return s3にアップロードする際に使用するバケット名
     */
    protected abstract String getBucketName();

    /**
     * s3にアップロードする際に使用するフォルダ名(最後に/は不要)(%1$s:処理対象年月(uuuuMM))を返却します。
     *
     * @return s3にアップロードする際に使用するフォルダ名(最後に/は不要)(%1$s:処理対象年月(uuuuMM))
     */
    protected abstract String getFolderName();

    /**
     * ポータルサイトに表示する内容(リンク名)を返却します。
     *
     * @param objectKey s3にアップロードしたオブジェクトキー
     * @return ポータルサイトに表示する内容(リンク名)
     */
    protected abstract String getDispReportName(String objectKey);

    /**
     * URLを設定するUsageHistoryBeanクラスのメソッド名を返却します。
     *
     * @param objectKey s3にアップロードしたオブジェクトキー
     * @return URLを設定するUsageHistoryBeanクラスのメソッド名
     */
    protected abstract String getMethodNameForURL(String objectKey);

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.common.base.AbstractClient#execute()
     */
    @Override
    protected void execute() {
        initialize();
        createReportList();
        uploadToS3();
        updatePortal();
    }

    /**
     * レポート出力処理の初期処理です。<br>
     * 本メソッドをサブクラスでオーバーライドする際は、継承先のメソッドの先頭で、
     * 本メソッドを呼び出す必要があります。
     */
    protected void initialize() {
        createTargetYearMonth();
        if (reportHelper == null) {
            reportHelper = new ReportHelper(getReportType(), getTargetYearMonth());
        }
    }

    /**
     * 処理対象年月を作成します。
     */
    protected void createTargetYearMonth() {
        // 現在のシステム年月から差月数を加算したものを処理対象年月とする。
        YearMonth yearMonth = DateUtil.getNowOfYearMonth();
        setTargetYearMonth(yearMonth.plusMonths(getDiffMonth()));
    }

    /**
     * レポート出力処理です。
     */
    protected void createReportList() {
        if (logger.isDebugEnabled()) {
            logger.debug(toMessage("処理対象年月：" + DateUtil.toString(getTargetYearMonth())));
        }

        List<String> idList = getTargetLinkedAccountIdList();

        // 処理対象となる契約情報リストを取得する。
        List<ContractInformationBean> ciList = searchContractInformationList(idList);
        logger.info(toMessage("Number of ContractInformation is [" + ciList.size() + "]"));

        int totalCount = 0;
        int errorCount = 0;
        int successCount = 0;
        for (ContractInformationBean ciBean : ciList) {
            totalCount++;

            // 連結アカウントID毎にレポート作成処理を行う。
            String targetId = ciBean.getLinkedAccountId();
            if (createReport(targetId, ciBean)) {
                successCount++;
            } else {
                // 作成に失敗した場合は、エラーカウントをインクリメントし、処理を続行する。
                errorCount++;
            }
        }

        StringBuilder builder = new StringBuilder();
        builder.append("合計：").append(Integer.toString(totalCount)).append(" ");
        builder.append("正常：").append(Integer.toString(successCount)).append(" ");
        builder.append("異常：").append(Integer.toString(errorCount));
        logger.info(toMessage(builder.toString()));
    }

    /**
     * 処理対象とする連結アカウントIDをアプリケーション情報から取得し(カテゴリ=Report, キー名=targetLinkedAccountIds)、返却します。<br>
     * アプリケーション情報に設定されていない場合(全ての連結アカウントIDが対象の場合はアプリケーション情報に設定しない)は
     * nullを返却します。
     *
     * @return 処理対象とする連結アカウントID
     */
    protected List<String> getTargetLinkedAccountIdList() {
        List<String> targetLinkedAccountIdList = null;
        String category = "Report";
        String targetLinkedAccountIds = AppInfoUtil.getVal(category, "TargetLinkedAccountIds");
        if (!StringUtils.isEmpty(targetLinkedAccountIds)) {
            String[] targets = targetLinkedAccountIds.split(",");
            targetLinkedAccountIdList = Arrays.asList(targets);
        }
        return targetLinkedAccountIdList;
    }

    /**
     * サポートポータルより取得した契約情報リストを返却します。<br>
     * idListが設定されている場合、そのIDに該当する契約情報リストを返却します。<br>
     * idListがnull or 空リストだった場合、サポートポータルに存在する集計対象となる契約情報を全て返却します。<br>
     * なお、該当データが存在しなかった場合は、空リストを返却します。
     *
     * @param idList 連結アカウントIDリスト
     * @return サポートポータルより取得した契約情報リスト
     */
    protected abstract List<ContractInformationBean> searchContractInformationList(List<String> idList);

    /**
     * 連結アカウントID単位の処理を行い、レポートファイルを作成します。<br>
     * 作成に失敗した場合は、falseを返却します。
     *
     * @param targetId 処理対象の連結アカウントID
     * @param ciBean 処理対象の連結アカウントIDと対応する契約情報
     * @return true: 成功, false 失敗
     */
    protected abstract boolean createReport(String targetId, ContractInformationBean ciBean);

    /**
     * 出力したレポート(CSV)を追加します。
     *
     * @param csvReport レポート(CSV)
     */
    protected void addCsvReport(File csvReport) {
        reportHelper.addCsvReport(csvReport);
    }

    /**
     * 出力したレポート(Excel,PDF)を追加します。
     *
     * @param excelReport レポート(Excel)
     * @param pdfReport レポート(PDF)
     */
    protected void addExcelPdfReport(File excelReport, File pdfReport) {
        reportHelper.addExcelPdfReport(excelReport, pdfReport);
    }

    /**
     * 出力したレポートをs3にアップロードします。<br>
     * レポートが1つも出力されていない場合は、何もせず、処理を抜けます。
     */
    protected void uploadToS3() {
        if (reportHelper.isEmptyReport()) {
            // レポートが1つも出力されていない場合は処理を抜ける。
            return;
        }

        logger.info(toMessage("uploadToS3 start"));
        reportHelper.uploadToS3(getBucketName(), getFolderName());
    }

    /**
     * s3にアップロードしたレポートをポータルサイトに反映します。<br>
     * s3にレポートが1つもアップロードされていない場合は、何もせず、処理を抜けます。
     */
    protected void updatePortal() {
        if (reportHelper.isEmptyObjectKey()) {
            // レポートがs3に1つもアップデートされていない場合は処理を抜ける。
            return;
        }

        logger.info(toMessage("updatePortal start"));

        Map<String, UsageHistoryBean> usageHistoryMap = new HashMap<>();
        try {
            Map<String, String> urlMap = reportHelper.generate();

            for (Map.Entry<String, String> urlSet : urlMap.entrySet()) {
                String key = urlSet.getKey();
                String value = urlSet.getValue();
                logger.info(toMessage(key + "=" + value));

                // オブジェクト名から連結アカウントIDを抽出する。
                String objectKeyDelimiter = "_";
                int objectKeyIndex = 1;
                String[] accountArray = key.split(objectKeyDelimiter);
                if (accountArray.length <= objectKeyIndex) {
                    StringBuilder builder = new StringBuilder();
                    builder.append("連結アカウントIDが抽出できませんでした。設定ファイルを見直してください。 ");
                    builder.append("オブジェクトキー:").append(key).append(" ");
                    builder.append("区切り文字：").append(objectKeyDelimiter).append(" ");
                    builder.append("順序：").append(objectKeyIndex);
                    throw new CommonException(toMessage(builder.toString()));
                }
                String targetId = accountArray[objectKeyIndex];

                if (logger.isDebugEnabled()) {
                    logger.debug(toMessage(targetId, "ポータルへアップロードする利用実績を作成"));
                }

                UsageHistoryBean usageHistoryBean = usageHistoryMap.get(targetId);
                if (usageHistoryBean == null) {
                    usageHistoryBean = new UsageHistoryBean();
                    usageHistoryMap.put(targetId, usageHistoryBean);

                    usageHistoryBean.setLinkedAccountId(targetId);
                    usageHistoryBean.setName(DateUtil.toString(getTargetYearMonth()));
                }

                // 利用実績の更新項目をセット
                String dispReportName = getDispReportName(key);
                String methodNameForURL = getMethodNameForURL(key);
                String urlForPortalField = reportHelper.createURLForPortalField(dispReportName, value);
                Method method = usageHistoryBean.getClass().getMethod(methodNameForURL, String.class);
                method.invoke(usageHistoryBean, urlForPortalField);
            }

        } catch (InvalidKeySpecException | IOException | NoSuchMethodException | SecurityException
                | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            throw new CommonException(e);
        }

        if (!usageHistoryMap.isEmpty()) {
            // サポートポータルの利用実績を更新
            List<UsageHistoryBean> beanList = new ArrayList<UsageHistoryBean>();
            beanList.addAll(usageHistoryMap.values());
            updateURL(beanList);
        }
    }

    /**
     * サポートポータルの利用実績を更新します。
     *
     * @param beanList 利用実績リスト
     */
    protected void updateURL(List<UsageHistoryBean> beanList) {
        SforceManageDao.saveUsageHistory_URLUpdate(beanList);
    }

    /*
     * (非 Javadoc)
     * @see jp.co.nes.awssupport.common.base.AbstractClient#finish(boolean, java.lang.Throwable)
     */
    @Override
    protected void finish(boolean isSuccess, Throwable throwable) {
        // 処理成功、失敗に関係なく、全てのコネクションをクローズします。
        DbConnUtil.closeAll();
    }

    /**
     * 出力先ファイル名(パスも含む)を作成して、返却します。
     *
     * @param targetId 連結アカウントID
     * @param path 出力先パス(最後にセパレータは不要)(%1$s:処理対象年月(uuuuMM))
     * @param name 出力先ファイル名(パスは含まない)(%1$s:連結アカウントID %2$s:処理対象年月(uuuuMM))
     * @return 出力先ファイル名(パスも含む)
     */
    protected String createOutputFileName(String targetId, String path, String name) {
        // 出力先ディレクトリを作成する。
        String outputDirPath = String.format(path, DateUtil.toString(getTargetYearMonth()));
        File dir = new File(outputDirPath);
        if (!dir.exists()) {
            // 出力先ファイルの格納先フォルダが未作成の場合は、フォルダ作成を行う。
            dir.mkdirs();
        }

        // 出力先ファイル名を作成する。
        StringBuilder outputFileNameBuilder = new StringBuilder();
        outputFileNameBuilder.append(outputDirPath);
        outputFileNameBuilder.append(File.separator);
        outputFileNameBuilder.append(String.format(name, targetId, DateUtil.toString(getTargetYearMonth())));
        return outputFileNameBuilder.toString();
    }

    /**
     * ログに出力するメッセージを返却します。
     *
     * @param messageSuffix メッセージのサフィックス
     * @return メッセージ
     */
    protected String toMessage(String messageSuffix) {
        return toMessage(null, messageSuffix);
    }

    /**
     * ログに出力するメッセージを返却します。
     *
     * @param targetId 処理対象の連結アカウントID
     * @param messageSuffix メッセージのサフィックス
     * @return メッセージ
     */
    protected String toMessage(String targetId, String messageSuffix) {
        StringBuilder builder = new StringBuilder();
        builder.append(getProcessName());
        builder.append(":");
        if (targetId != null) {
            builder.append(targetId);
            builder.append(" ");
        }
        builder.append(messageSuffix);
        return builder.toString();
    }
}
